# frozen_string_literal: true

shared_examples "comments" do
  let!(:organization) { create(:organization) }
  let!(:user) { create(:user, :confirmed, organization:) }
  let!(:comments) { create_list(:comment, 3, commentable:) }

  before do
    switch_to_host(organization.host)
  end

  after do
    expect_no_js_errors
  end

  it "shows the list of comments for the resource" do
    visit resource_path

    expect(page).to have_css("#comments")
    expect(page).to have_css(".comment", count: comments.length)

    within "#comments" do
      comments.each do |comment|
        expect(page).to have_content comment.author.name
        expect(page).to have_content comment.body.values.first
      end
    end
  end

  it "allows user to sort the comments", :slow do
    comment = create(:comment, commentable:, body: "Most Rated Comment")
    create(:comment_vote, comment:, author: user, weight: 1)

    visit resource_path

    expect(page).to have_no_content("Comments are disabled at this time")

    expect(page).to have_css(".comment", minimum: 1)

    within ".comment-order-by" do
      select "Best rated", from: "order"
    end

    expect(page).to have_css(".comments > div:nth-child(2)", text: "Most Rated Comment")
  end

  context "when there are comments and replies" do
    let!(:single_comment) { create(:comment, commentable:) }
    let!(:reply) { create(:comment, commentable: single_comment, root_commentable: commentable) }

    it "displays the show replies link on comment with reply" do
      visit resource_path
      expect(page).to have_no_content("Comments are disabled at this time")
      expect(page).to have_css(".comment", minimum: 1)

      within("#accordion-#{single_comment.id}") do
        expect(page).to have_content "1 answer"
      end
    end

    context "when there is a comment with the same parent id but different type with replies" do
      let!(:other_component) { create(:component, manifest_name: :dummy, organization:) }
      let!(:other_commentable) { create(:dummy_resource, component: other_component, author: user, id: single_comment.id) }
      let!(:reply) { create(:comment, commentable: other_commentable, root_commentable: other_commentable) }
      let!(:other_reply) { create(:comment, commentable: reply, root_commentable: other_commentable) }

      it "displays the show replies link on comment with reply" do
        visit resource_path
        expect(page).to have_no_content("Comments are disabled at this time")
        expect(page).to have_css(".comment", minimum: 1)

        within("#accordion-#{single_comment.id}") do
          expect(page).to have_no_content "Hide reply"
        end
      end
    end
  end

  context "when there are deleted comments" do
    let(:deleted_comment) { comments[0] }

    before do
      deleted_comment.delete!
      visit resource_path
    end

    it "shows only a deletion message for deleted comments" do
      expect(page).to have_css("#comment_#{deleted_comment.id}")

      expect(page).to have_no_content(deleted_comment.author.name)
      expect(page).to have_no_content(translated(deleted_comment.body))
      within "#comment_#{deleted_comment.id}" do
        expect(page).to have_content("Comment deleted on")
        expect(page).to have_no_css(".comment__header")
      end
    end

    it "counts only not deleted comments" do
      expect(page).to have_css("span.comments-count", text: "#{comments.length - 1} comments")
    end

    context "when deleted comment has replies, they are shown" do
      let!(:reply) { create(:comment, commentable: deleted_comment, root_commentable: commentable, body: "Please, delete your comment") }

      it "shows replies of deleted comments" do
        visit resource_path

        within "#comment_#{deleted_comment.id}" do
          expect(page).to have_css("#comment-#{deleted_comment.id}-replies")
          expect(page).to have_content(reply.author.name)
          expect(page).to have_content(reply.body.values.first)
        end
      end
    end
  end

  context "when not authenticated" do
    it "does not show form to add comments to user" do
      visit resource_path
      expect(page).to have_no_css(".add-comment form")
      expect(page).to have_css(".comment-thread")
    end

    context "when user visit a mobile browser" do
      before do
        driven_by(:iphone)
        switch_to_host(organization.host)
        visit decidim.root_path
        click_on "Accept all"
        visit resource_path
      end

      it "does not show the add comment button" do
        expect(page).to have_no_content("Add comment")
      end

      it "shows a message so user can Log in or create an account" do
        expect(page).to have_content("Log in or create an account to add your comment.")
      end
    end
  end

  context "when authenticated" do
    before do
      login_as user, scope: :user
      visit resource_path
    end

    it "shows form to add comments to user" do
      expect(page).to have_css(".add-comment form")
    end

    context "when user visit a computer browser" do
      before do
        switch_to_host(organization.host)
        visit decidim.root_path
        login_as user, scope: :user
        visit resource_path
      end

      it "does not show a modal with form to add comments" do
        expect(page).to have_no_css(".fullscreen")
      end

      it "does not show the add comment button" do
        expect(page).to have_no_content("Add comment")
      end

      it "allows user to comment" do
        find("textarea[name='comment[body]']").set("Test comment with a computer.")
        click_on "Publish comment"
        expect(page).to have_content("Test comment with a computer.")
      end
    end

    context "when user visit a mobile browser" do
      before do
        driven_by(:iphone)
        switch_to_host(organization.host)
        visit decidim.root_path
        click_on "Accept all"
        login_as user, scope: :user
        visit resource_path
        if page.has_content?("Log in")
          login_as user, scope: :user
          visit resource_path
        end
      end

      it "does not show a message so user can Log in or create an account" do
        expect(page).to have_no_content("Log in or create an account to add your comment.")
      end

      it "shows a modal with the comment form" do
        expect(page).to have_content("Add comment")
        click_on "Add comment"

        expect(page).to have_content("Add comment")
        expect(page).to have_content("1000 characters left")
        expect(page).to have_css(".add-comment form")
        expect(page).to have_css(".fullscreen")

        find("textarea[name='comment[body]']").set("Test comment with a mobile phone.")
        click_on "Publish comment"
        expect(page).to have_content("Test comment with a mobile phone.")
      end
    end

    context "when user is not authorized to comment" do
      let(:permissions) do
        {
          comment: {
            authorization_handlers: {
              "dummy_authorization_handler" => { "options" => {} }
            }
          }
        }
      end

      before do
        organization.available_authorizations = ["dummy_authorization_handler"]
        organization.save!
        commentable.create_resource_permission(permissions:)
        allow(commentable).to receive(:user_allowed_to_comment?).with(user).and_return(false)
        allow(commentable).to receive(:user_authorized_to_comment?).with(user).and_return(true)
      end

      it "shows a message indicating that comments are restricted" do
        visit resource_path
        expect(page).to have_no_content("Comments are disabled at this time")
        expect(page).to have_content("You need to be verified to comment at this moment")
      end
    end

    describe "when using emojis" do
      before do
        skip("This spec does not work in focus mode, since there is no language selector.") if has_selector?(".main-bar--focus-mode-back-button")

        within_language_menu do
          click_on "Castellano"
        end
      end
      shared_examples_for "allowing to select emojis" do
        it "allows selecting emojis" do
          within_language_menu do
            click_on locale
          end

          sleep 1
          page.scroll_to(find(".add-comment form"))

          within ".add-comment form" do
            expect(page).to have_css(".emoji__container")
            expect(page).to have_css(".emoji__trigger .emoji__button")
            find(".emoji__trigger .emoji__button").click
          end

          within ".emoji__decidim" do
            expect(page).to have_content(phrase)
            # Since emoji-mart is a React component, we need to use JS to click on an emoji icon
            # as the emoji picker is a shadow DOM element.
            # The script below is trying to find the first emoji in the "Smileys & People" category and simulate
            # a click from the user on it.
            script = <<~JS
              var emoji_picker = document.getElementsByTagName("em-emoji-picker")[0];
              var category = emoji_picker.shadowRoot.querySelectorAll("div.category")[1]
              category.querySelectorAll("button")[0].click();
            JS
            execute_script(script)
          end

          within ".add-comment form" do
            expect(find("textarea").value.strip).to have_content("😀")
          end
        end
      end

      context "when the locale is supported" do
        let(:locale) { "English" }
        let(:phrase) { I18n.t("emojis.categories.people") }

        it_behaves_like "allowing to select emojis"
      end

      context "when the locale is not supported" do
        let(:locale) { "Català" }
        let(:phrase) { I18n.with_locale(:ca) { I18n.t("emojis.categories.people") } }

        around do |example|
          I18n.with_locale(:ca) do
            example.run
          end
        end

        it_behaves_like "allowing to select emojis"
      end
    end

    context "when no default comments length specified" do
      it "displays the numbers of characters left" do
        within ".add-comment form" do
          expect(page).to have_content("1000 characters left")
        end
      end
    end

    context "when organization has a default comments length params" do
      let!(:organization) { create(:organization, comments_max_length: 2000) }

      it "displays the numbers of characters left" do
        within ".add-comment form" do
          expect(page).to have_content("2000 characters left")
        end
      end

      it "updates the numbers of characters left correctly" do
        within "form#new_comment_for_#{commentable.commentable_type.demodulize}_#{commentable.id}" do
          field = find("#add-comment-#{commentable.commentable_type.demodulize}-#{commentable.id}")
          field.set " "
          field.native.send_keys "This is a new comment."

          expect(page).to have_content("1977 characters left")
        end
      end

      context "when working with the screen reader character counter" do
        let(:field_id) { "add-comment-#{commentable.commentable_type.demodulize}-#{commentable.id}" }
        let(:field) { page.find("##{field_id}") }

        before do
          # Make sure the focus stays in the comment form during these tests
          # because only then the screen reader
          page.execute_script("document.getElementById('#{field_id}').focus()")
        end

        it "updates the numbers of characters left correctly for screen reader" do
          within "form#new_comment_for_#{commentable.commentable_type.demodulize}_#{commentable.id}" do
            fill_in field_id, with: "This is a new comment."

            # The screen reader character counter should update only when the user
            # reaches 10% interval of the total characters available not to
            # announce the remaining characters after every keystroke.
            field.native.send_keys " Sending some new text."
            within ".remaining-character-count" do
              expect(page).to have_content("1955 characters left") # Normal
            end
            within ".remaining-character-count-sr" do
              expect(page).to have_content("2000 characters left") # Screen reader
            end

            # After 10% of the total characters is reached, it should be updated
            # to the screen reader section to announce it.
            field.native.send_keys "a" * 155
            within ".remaining-character-count" do
              expect(page).to have_content("1800 characters left") # Normal
            end
            within ".remaining-character-count-sr" do
              expect(page).to have_content("1800 characters left") # Screen reader
            end

            # After continuing typing after the announcement, the screen reader
            # characters should stay the same (announced on the next 10%
            # interval).
            field.native.send_keys "b"
            within ".remaining-character-count" do
              expect(page).to have_content("1799 characters left") # Normal
            end
            within ".remaining-character-count-sr" do
              expect(page).to have_content("1800 characters left") # Screen reader
            end

            # When text is removed at the interval, the screen reader should
            # update back to the previous interval.
            field.native.send_keys [:backspace, :backspace, :backspace, :backspace]
            within ".remaining-character-count" do
              expect(page).to have_content("1803 characters left") # Normal
            end
            within ".remaining-character-count-sr" do
              expect(page).to have_content("1800 characters left") # Screen reader
            end

            # After continuing typing after the removal of characters, we should
            # stay in the "latest announcement" not to confuse the user as
            # - "1800 characters left" (actual 1803)
            # - Type in one character
            # - "1900 characters left" (actual 1802)
            field.native.send_keys "b"
            within ".remaining-character-count" do
              expect(page).to have_content("1802 characters left") # Normal
            end
            within ".remaining-character-count-sr" do
              expect(page).to have_content("1800 characters left") # Screen reader
            end

            # After the input is blurred, the screen reader character counter
            # should show the actual amount of characters left.
            page.execute_script("document.getElementById('#{field_id}').blur()")
            within ".remaining-character-count" do
              expect(page).to have_content("1802 characters left") # Normal
            end
            within ".remaining-character-count-sr" do
              expect(page).to have_content("1802 characters left") # Screen reader
            end
          end
        end

        context "when reaching the announce after every threshold" do
          it "updates the numbers of characters left correctly for screen reader" do
            within "form#new_comment_for_#{commentable.commentable_type.demodulize}_#{commentable.id}" do
              # Test that when reaching the "announce after every" threshold, the
              # characters are announced after every keystroke.
              field = find("#add-comment-#{commentable.commentable_type.demodulize}-#{commentable.id}")
              field.set "a" * 1989
              within ".remaining-character-count" do
                expect(page).to have_content("11 characters left") # Normal
              end
              within ".remaining-character-count-sr" do
                expect(page).to have_content("200 characters left") # Screen reader
              end

              (2..10).reverse_each do |remaining|
                field.native.send_keys "b"
                within ".remaining-character-count-sr" do
                  expect(page).to have_content("#{remaining} characters left")
                end
              end

              field.native.send_keys "b"
              within ".remaining-character-count-sr" do
                expect(page).to have_content("1 character left")
              end

              field.native.send_keys "c"
              within ".remaining-character-count-sr" do
                expect(page).to have_content("0 characters left")
              end

              # Test that the SR counter will stick at the last announced
              # threshold if the next threshold is not hit and text is removed.
              # This prevents weird announcements such as:
              # - 0 characters left
              # - Remove 10 characters
              # - 10 characters left
              # - Remove 1 character
              # - 200 characters left
              page.execute_script("document.getElementById('#{field_id}').setSelectionRange(1850, 2000)")
              field.native.send_keys [:backspace]
              within ".remaining-character-count" do
                expect(page).to have_content("150 characters left") # Normal
              end
              within ".remaining-character-count-sr" do
                expect(page).to have_content("0 characters left") # Screen reader
              end

              field.native.send_keys "d"
              within ".remaining-character-count-sr" do
                expect(page).to have_content("0 characters left")
              end
            end
          end
        end

        context "when deleting text the announce after every threshold" do
          it "updates the numbers of characters left correctly for screen reader" do
            within ".add-comment form" do
              fill_in field_id, with: "a" * 2000
              within ".remaining-character-count" do
                expect(page).to have_content("0 characters left") # Normal
              end
              within ".remaining-character-count-sr" do
                expect(page).to have_content("0 characters left") # Screen reader
              end

              # Test that the SR counter updates correctly after hitting the
              # next threshold.
              page.execute_script("document.getElementById('#{field_id}').setSelectionRange(1800, 2000)")
              field.native.send_keys [:backspace]
              within ".remaining-character-count" do
                expect(page).to have_content("200 characters left") # Normal
              end
              within ".remaining-character-count-sr" do
                expect(page).to have_content("200 characters left") # Screen reader
              end

              # The SR counter should stay at the correct boundary.
              field.native.send_keys [:backspace, :backspace]
              within ".remaining-character-count" do
                expect(page).to have_content("202 characters left") # Normal
              end
              within ".remaining-character-count-sr" do
                expect(page).to have_content("200 characters left") # Screen reader
              end

              # It stays at the correct boundary when starting to type again.
              field.native.send_keys "b"
              within ".remaining-character-count" do
                expect(page).to have_content("201 characters left") # Normal
              end
              within ".remaining-character-count-sr" do
                expect(page).to have_content("200 characters left") # Screen reader
              end

              field.native.send_keys "c"
              within ".remaining-character-count" do
                expect(page).to have_content("200 characters left") # Normal
              end
              within ".remaining-character-count-sr" do
                expect(page).to have_content("200 characters left") # Screen reader
              end

              field.native.send_keys "d"
              within ".remaining-character-count" do
                expect(page).to have_content("199 characters left") # Normal
              end
              within ".remaining-character-count-sr" do
                expect(page).to have_content("200 characters left") # Screen reader
              end
            end
          end
        end
      end

      context "when component is present and has a default comments length params" do
        it "displays the numbers of characters left" do
          if component.present?
            component.update!(settings: { comments_max_length: 3000 })
            visit current_path

            within "form#new_comment_for_#{commentable.commentable_type.demodulize}_#{commentable.id}" do
              expect(page).to have_content("3000 characters left")
            end
          end
        end

        it "let the emoji button works properly when there are not too much characters" do
          if component.present?
            component.update!(settings: { comments_max_length: 100 })
            visit current_path

            within "form#new_comment_for_#{commentable.commentable_type.demodulize}_#{commentable.id}" do
              field = find("#add-comment-#{commentable.commentable_type.demodulize}-#{commentable.id}")
              field.set " "
              field.native.send_keys "toto"
            end

            expect(page).to have_no_css(".emoji__decidim")
            within "form#new_comment_for_#{commentable.commentable_type.demodulize}_#{commentable.id}" do
              find(".emoji__button").click
            end
            expect(page).to have_css(".emoji__decidim")
          end
        end

        it "deactivate the emoji button when there are less than 4 characters left" do
          if component.present?
            component.update!(settings: { comments_max_length: 30 })
            visit current_path

            within "form#new_comment_for_#{commentable.commentable_type.demodulize}_#{commentable.id}" do
              field = find("#add-comment-#{commentable.commentable_type.demodulize}-#{commentable.id}")
              field.set " "
              field.native.send_keys("0123456789012345678901234567")
              find(".emoji__button").click
            end
            expect(page).to have_no_css(".emoji-picker__picker.emoji-picker__picker")
          end
        end
      end
    end

    context "when user adds a new comment" do
      let(:content) { "This is a new comment" }

      before do
        within "form#new_comment_for_#{commentable.commentable_type.demodulize}_#{commentable.id}" do
          field = find("#add-comment-#{commentable.commentable_type.demodulize}-#{commentable.id}")
          field.set " "
          field.native.send_keys content
          click_on "Publish comment"
        end

        expect(page).to have_content(content)
      end

      it "shows comment to the user, updates the comments counter and clears the comment textarea" do
        expect(page).to have_comment_from(user, content, wait: 20)
        expect(page).to have_css("span.comments-count", text: "#{commentable.comments.count} comments")
        expect(page.find("#add-comment-#{commentable.commentable_type.demodulize}-#{commentable.id}").value).to be_empty
      end

      it "shows the entry in last activities" do
        visit decidim.last_activities_path
        expect(page).to have_content("New comment: #{content}")

        within "#filters" do
          find("a", class: "filter", text: "Comment", match: :first).click
        end
        expect(page).to have_content("New comment: #{content}")
      end
    end

    context "when user adds a new comment with a link" do
      let(:content) { "Very nice http://www.debian.org linux distro" }

      before do
        within "form#new_comment_for_#{commentable.commentable_type.demodulize}_#{commentable.id}" do
          field = find("#add-comment-#{commentable.commentable_type.demodulize}-#{commentable.id}")
          field.set " "
          field.native.send_keys content
          click_on "Publish comment"
        end
      end

      it "adds external link css" do
        expect(page).to have_css("a", text: "http://www.debian.org")
        within("a", text: "http://www.debian.org") do
          expect(page).to have_text "(External link)"
        end
      end

      it "changes link to point to /link" do
        expect(page).to have_link("http://www.debian.org", href: "/link?external_url=http%3A%2F%2Fwww.debian.org%2F")
      end
    end

    context "when the user is writing a new comment while someone else comments" do
      let(:new_comment_body) { "Hey, I just jumped in the conversation!" }
      let(:new_comment) { build(:comment, commentable:, body: new_comment_body) }
      let(:content) { "This is a new comment" }

      before do
        within "form#new_comment_for_#{commentable.commentable_type.demodulize}_#{commentable.id}" do
          field = find("#add-comment-#{commentable.commentable_type.demodulize}-#{commentable.id}")
          field.set " "
          field.native.send_keys content
        end
        new_comment.save!
      end

      it "does not clear the current user's comment" do
        expect(page).to have_content(new_comment.body.values.first, wait: 20)
        expect(page.find("#add-comment-#{commentable.commentable_type.demodulize}-#{commentable.id}").value).to include(content)
      end

      context "when user can hide replies on a thread" do
        let(:thread) { comments.first }
        let(:new_reply_body) { "Hey, I just jumped inside the thread!" }
        let!(:new_reply) { create(:comment, commentable: thread, root_commentable: commentable, body: new_reply_body) }

        it "displays a way to to display content" do
          visit current_path
          within "#comment_#{thread.id}" do
            expect(page).to have_content("1 answer")
            click_on "1 answer"
            expect(page).to have_content(new_reply_body)
            click_on "Reply", match: :first
            expect(page).to have_content("Publish reply")
            find("textarea[name='comment[body]']").set("Test reply comments.")
            click_on "Publish reply"
            expect(page).to have_content("Show 2 replies")
            click_on "Show 2 replies"
            expect(page).to have_content("Test reply comments.")
          end
        end

        it "displays a way hide content" do
          visit current_path
          within "#comment_#{thread.id}" do
            expect(page).to have_content("1 answer")
            click_on "1 answer"
            expect(page).to have_content("1 answer")
            click_on "1 answer"
            expect(page).to have_no_content(new_reply_body)
          end
        end

        context "when are more replies" do
          let!(:new_replies) { create_list(:comment, 2, commentable: thread, root_commentable: commentable, body: new_reply_body) }

          it "displays the show button" do
            visit current_path
            within "#comment_#{thread.id}" do
              expect(page).to have_content("3 answers")
              expect(page).to have_no_content(new_reply_body)
              click_on "3 answers"
              expect(page).to have_content(new_reply_body)
            end
          end
        end
      end

      context "when inside a thread reply form" do
        let(:thread) { comments.first }
        let(:new_reply_body) { "Hey, I just jumped inside the thread!" }
        let(:new_reply) { build(:comment, commentable: thread, root_commentable: commentable, body: new_reply_body) }
        let(:reply_content) { "This is a new reply" }

        before do
          within "div#comment_#{thread.id}" do
            find("span", text: "Reply").click
          end

          within "form#new_comment_for_#{thread.commentable_type.demodulize}_#{thread.id}" do
            field = find("#add-comment-#{thread.commentable_type.demodulize}-#{thread.id}")
            field.set " "
            field.native.send_keys reply_content
          end
          new_reply.save!
        end

        it "does not clear the current user's comment" do
          expect(page).to have_content(new_reply.body.values.first, wait: 20)
          expect(page.find("#add-comment-#{commentable.commentable_type.demodulize}-#{commentable.id}").value).to include(content)
          expect(page.find("#add-comment-#{thread.commentable_type.demodulize}-#{thread.id}").value).to include(reply_content)
        end
      end
    end

    context "when a user deletes a comment" do
      let(:comment_body) { "This comment is a mistake" }
      let!(:comment) { create(:comment, body: comment_body, commentable:, author: comment_author) }

      before do
        visit resource_path
      end

      context "when the comment is not authored by user" do
        let!(:comment_author) { create(:user, :confirmed, organization:) }

        it "the context menu of the comment does not show a delete link" do
          within "#comment_#{comment.id}" do
            page.find("[id^='dropdown-trigger']").click
            expect(page).to have_no_link("Delete")
          end
        end
      end

      context "when the comment is authored by user" do
        let(:comment_author) { user }

        it "the context menu of the comment shows a delete link" do
          within "#comment_#{comment.id}" do
            page.find("[id^='dropdown-trigger']").click
            expect(page).to have_link("Delete")
          end
        end

        it "the user can delete the comment and updates the comments counter" do
          expect(Decidim::Comments::Comment.not_deleted.count).to eq(4)

          within "#comment_#{comment.id}" do
            page.find("[id^='dropdown-trigger']").click
            click_on "Delete"
          end

          accept_confirm

          expect(page).to have_css("#comment_#{comment.id}")
          within "#comment_#{comment.id}" do
            expect(page).to have_content("Comment deleted on")
            expect(page).to have_no_content comment_author.name
            expect(page).to have_no_css(".comment__header")
          end
          expect(page).to have_css("span.comments-count", text: "3 comments")

          expect(Decidim::Comments::Comment.not_deleted.count).to eq(3)
        end
      end
    end

    context "when a user edits a comment" do
      let(:comment_body) { "This coment has a typo" }
      let!(:comment) { create(:comment, body: comment_body, commentable:, author: comment_author) }

      before do
        visit resource_path
      end

      context "when the comment is not authored by user" do
        let!(:comment_author) { create(:user, :confirmed, organization:) }

        it "the context menu of the comment does not show an edit button" do
          within "#comment_#{comment.id}" do
            # Toolbar
            page.find("[id^='dropdown-trigger']").click
            expect(page).to have_no_button("Edit")
          end
        end
      end

      context "when the comment is authored by user" do
        let!(:comment_author) { user }

        it "the context menu of the comment show an edit button" do
          within "#comment_#{comment.id}" do
            # Toolbar
            page.find("[id^='dropdown-trigger']").click
            expect(page).to have_button("Edit")
          end
        end

        context "when the user edits a comment" do
          before do
            within "#comment_#{comment.id}" do
              # Toolbar
              page.find("[id^='dropdown-trigger']").click
              click_on "Edit"
            end
            fill_in "edit_comment_#{comment.id}", with: " This comment has been fixed"
            click_on "Send"
          end

          it "the comment body changes" do
            within "#comment_#{comment.id}" do
              expect(page).to have_content("This comment has been fixed")
              expect(page).to have_no_content(comment_body)
            end
          end

          it "the header of the comment displays an edited message" do
            within "#comment_#{comment.id}" do
              expect(page).to have_content("Edited")
            end
          end

          it "has only one edit modal" do
            expect(page).to have_css("#editCommentModal#{comment.id}", visible: :hidden, count: 1)
            3.times do |index|
              sleep 2
              within "#comment_#{comment.id}" do
                page.find("[id^='dropdown-trigger']").click
                click_on "Edit"
              end
              fill_in "edit_comment_#{comment.id}", with: " This comment has been edited #{1 + index} times"
              click_on "Send"
            end
            expect(page).to have_css("#editCommentModal#{comment.id}", visible: :all, count: 1)
          end
        end
      end
    end

    context "when a user replies to a comment", :slow do
      let!(:comment_author) { create(:user, :confirmed, organization:) }
      let!(:comment) { create(:comment, commentable:, author: comment_author) }
      let(:content) { "This is a reply" }

      it "shows reply to the user" do
        visit resource_path

        within "#comments #comment_#{comment.id}" do
          click_on "Reply"
        end

        expect(page).to have_css("#comment_#{comment.id} .add-comment")

        within "form#new_comment_for_#{comment.commentable_type.demodulize}_#{comment.id}" do
          field = find("#add-comment-#{comment.commentable_type.demodulize}-#{comment.id}")
          field.set " "
          field.native.send_keys content
          click_on "Publish reply"
        end

        expect(page).to have_reply_to(comment, content)
        expect(page).to have_css("span.comments-count", text: "#{commentable.comments.count} comments")
        expect(page).to have_reply_to(comment, "This is a reply")
      end
    end

    context "when a comment has been moderated" do
      let!(:parent) { create(:comment, commentable:) }
      let!(:reply) { create(:comment, commentable: parent, root_commentable: commentable) }

      it "does not show additional reply" do
        Decidim::Moderation.create!(reportable: reply, participatory_space: reply.participatory_space, hidden_at: 1.day.ago)

        visit current_path

        within "#comments #comment_#{parent.id}" do
          expect(page).to have_css("#comment-#{parent.id}-replies")
          expect(page.find("#comment-#{parent.id}-replies").text).to be_blank
        end
      end

      context "when admin moderates the comment" do
        let!(:user) { create(:user, :admin, :confirmed, organization:) }

        before do
          switch_to_host(organization.host)
          login_as user, scope: :user
          visit resource_path
        end

        it "hides the comment" do
          within "#comment_#{comments.first.id}" do
            page.find("[id^='dropdown-trigger']").click
            click_on "Report"
          end

          within "#flagModalComment#{comments.first.id}" do
            check "Hide this content"
            click_on "Hide"
          end

          expect(page).to have_content("This resource has been hidden.")
        end
      end
    end

    describe "arguable option" do
      context "when commenting with alignment" do
        before do
          visit resource_path

          expect(page).to have_css(".add-comment form")
        end
      end
    end

    describe "votable option" do
      before do
        visit resource_path
      end

      context "when upvoting a comment" do
        it "works according to the setting in the commentable" do
          within "#comment_#{comments[0].id}" do
            if commentable.comments_have_votes?
              expect(page).to have_css(".js-comment__votes--up", text: /0/)
              page.find(".js-comment__votes--up").click
              expect(page).to have_css(".js-comment__votes--up", text: /1/)
            else
              expect(page).to have_no_css(".js-comment__votes--up", text: /0/)
            end
          end
        end

        context "when the comment has a thread" do
          let!(:comment_on_comment) { create(:comment, :comment_on_comment, commentable: comments[0], root_commentable: comments[0].commentable) }

          it "does not increase the votes for the children of the upvoting comment" do
            skip "Commentable comments has no votes" unless commentable.comments_have_votes?

            visit current_path
            expect(page).to have_css("#comment_#{comments[0].id} > [data-comment-footer] > .comment__footer-grid .comment__votes .js-comment__votes--up", text: /0/, visible: :all)
            page.find("#comment_#{comments[0].id} > [data-comment-footer] > .comment__footer-grid .comment__votes .js-comment__votes--up").click
            expect(page).to have_css("#comment_#{comments[0].id} > [data-comment-footer] > .comment__footer-grid .comment__votes .js-comment__votes--up", text: /1/, visible: :all)
            expect(page).to have_css("#comment_#{comment_on_comment.id} > [data-comment-footer] >.comment__footer-grid .comment__votes .js-comment__votes--up", text: /0/,
                                                                                                                                                                visible: :all)
          end
        end
      end

      context "when downvoting a comment" do
        it "works according to the setting in the commentable" do
          within "#comment_#{comments[0].id}" do
            if commentable.comments_have_votes?
              expect(page).to have_css(".js-comment__votes--down", text: /0/)
              page.find(".js-comment__votes--down").click
              expect(page).to have_css(".js-comment__votes--down", text: /1/)
            else
              expect(page).to have_no_css(".js-comment__votes--down", text: /0/)
            end
          end
        end
      end
    end

    describe "mentions drop-down", :slow do
      before do
        visit resource_path

        within "form#new_comment_for_#{commentable.commentable_type.demodulize}_#{commentable.id}" do
          field = find("#add-comment-#{commentable.commentable_type.demodulize}-#{commentable.id}")
          field.set " "
          field.native.send_keys content
        end
      end

      context "when mentioning a valid user" do
        let!(:mentioned_user) { create(:user, :confirmed, organization:) }
        let(:content) { "A valid user mention: @#{mentioned_user.nickname}" }

        context "when text finish with a mention" do
          it "shows the tribute container" do
            expect(page).to have_css(".tribute-container", text: mentioned_user.name, wait: 10)
          end
        end

        context "when text contains a mention" do
          let(:content) { "A valid user mention: @#{mentioned_user.nickname}." }

          it "shows the tribute container" do
            expect(page).to have_no_css(".tribute-container", text: mentioned_user.name)
          end
        end
      end

      context "when mentioning a non valid user" do
        let!(:mentioned_user) { create(:user, organization:) }
        let(:content) { "A unconfirmed user mention: @#{mentioned_user.nickname}" }

        it "do not show the tribute container" do
          expect(page).to have_no_css(".tribute-container", text: mentioned_user.name)
        end
      end
    end

    describe "mentions", :slow do
      before do
        visit resource_path

        within "form#new_comment_for_#{commentable.commentable_type.demodulize}_#{commentable.id}" do
          field = find("#add-comment-#{commentable.commentable_type.demodulize}-#{commentable.id}")
          field.set " "
          field.native.send_keys content
          click_on "Publish comment"
        end
      end

      context "when mentioning a valid user" do
        let!(:mentioned_user) { create(:user, :confirmed, organization:) }
        # do not finish with the mention to avoid trigger the drop-down
        let(:content) { "A valid user mention: @#{mentioned_user.nickname}." }

        it "replaces the mention with a link to the user's profile" do
          expect(page).to have_comment_from(user, "A valid user mention: @#{mentioned_user.nickname}", wait: 20)
          expect(page).to have_link "@#{mentioned_user.nickname}", href: "http://#{mentioned_user.organization.host}:#{Capybara.server_port}/profiles/#{mentioned_user.nickname}"
        end
      end

      context "when mentioning an existing user outside current organization" do
        let!(:mentioned_user) { create(:user, :confirmed, organization: create(:organization)) }
        let(:content) { "This text mentions a user outside current organization: @#{mentioned_user.nickname}" }

        it "ignores the mention" do
          expect(page).to have_comment_from(user, "This text mentions a user outside current organization: @#{mentioned_user.nickname}", wait: 20)
          expect(page).to have_no_link "@#{mentioned_user.nickname}"
        end
      end

      context "when mentioning a non valid user" do
        let(:content) { "This text mentions a @nonexistent user" }

        it "ignores the mention" do
          expect(page).to have_comment_from(user, "This text mentions a @nonexistent user", wait: 20)
          expect(page).to have_no_link "@nonexistent"
        end
      end
    end

    describe "export_serializer" do
      let(:comment) { comments.first }

      it "returns the serializer for the comment" do
        expect(comment.class.export_serializer).to eq(Decidim::Comments::CommentSerializer)
      end

      context "with instance" do
        subject { comment.class.export_serializer.new(comment).serialize }

        it { is_expected.to have_key(:id) }
        it { is_expected.to have_key(:created_at) }
        it { is_expected.to have_key(:body) }
        it { is_expected.to have_key(:locale) }
        it { is_expected.to have_key(:author) }
        it { is_expected.to have_key(:alignment) }
        it { is_expected.to have_key(:depth) }
        it { is_expected.to have_key(:commentable_id) }
        it { is_expected.to have_key(:commentable_type) }
        it { is_expected.to have_key(:root_commentable_url) }
      end
    end
  end
end

shared_examples "comments blocked" do
  context "when not authenticated" do
    context "when comments are blocked" do
      let(:active_step_id) { component.participatory_space.active_step.id }

      before do
        component.update!(step_settings: { active_step_id => { comments_blocked: true } })
      end

      it "shows a message indicating that comments are disabled" do
        visit resource_path
        expect(page).to have_content("Comments are currently disabled, only administrators can reply or post new ones.")
        expect(page).to have_no_content("You need to be verified to comment at this moment")
      end
    end
  end

  context "when authenticated" do
    let!(:user) { create(:user, :confirmed, organization:) }
    let!(:comments) { create_list(:comment, 3, commentable:) }

    before do
      login_as user, scope: :user
      visit resource_path
    end

    shared_examples "can answer comments" do
      it "can answer" do
        visit resource_path
        expect(page).to have_link("Comment")
        page.find("a", text: "Comment").click
        find("textarea[name='comment[body]']").set("Test admin commenting in a closed comment.")
        click_on "Publish comment"
        expect(page).to have_content("Test admin commenting in a closed comment.")

        expect(page).to have_button("Reply")
        first("button", text: "Reply").click
        expect(page).to have_css(".comment-thread")
        within first(".comment-thread") do
          find("textarea[name='comment[body]']").set("Test admin replying a closed comment.")
          click_on "Publish reply"
        end
        expect(page).to have_content("Test admin replying a closed comment.")
      end
    end

    context "when comments are blocked" do
      let(:active_step_id) { component.participatory_space.active_step.id }

      before do
        component.update!(step_settings: { active_step_id => { comments_blocked: true } })
      end

      it "shows a message indicating that comments are disabled" do
        visit resource_path
        expect(page).to have_content("Comments are currently disabled, only administrators can reply or post new ones.")
        expect(page).to have_no_content("You need to be verified to comment at this moment")
      end

      context "when the user is an administrator" do
        let!(:user) { create(:user, :admin, :confirmed, organization:) }

        it_behaves_like "can answer comments"
      end

      context "when the user has a role of user manager" do
        let!(:user) { create(:user, :user_manager, :confirmed, organization:) }

        it_behaves_like "can answer comments"
      end

      context "when the user has an evaluator role in the same participatory space" do
        let!(:evaluator_role) { create(:participatory_process_user_role, role: :evaluator, user:, participatory_process: participatory_space) }

        it_behaves_like "can answer comments"
      end

      shared_examples "evaluator role in different participatory space" do |space_type|
        let!(:another_space_evaluator_role) do
          create(:"#{space_type}_user_role", role: :evaluator, user:, "#{space_type}": create(space_type, organization:))
        end

        it "cannot answer" do
          visit resource_path
          expect(page).to have_content("Comments are currently disabled, only administrators can reply or post new ones.")
          expect(page).to have_no_content("You need to be verified to comment at this moment")
          expect(page).to have_no_css("textarea#add-comment-Proposal-1")
        end
      end

      context "when the user has an evaluator role in a different participatory space" do
        include_examples "evaluator role in different participatory space", :participatory_process
        include_examples "evaluator role in different participatory space", :conference
        include_examples "evaluator role in different participatory space", :assembly
      end
    end
  end
end

shared_examples "comments with two columns" do
  let!(:user) { create(:user, :confirmed, organization:) }

  before do
    login_as user, scope: :user
  end

  context "displays comments list in two columns" do
    let!(:comments_in_favor) { create_list(:comment, 2, :in_favor, commentable:) }
    let!(:comments_against) { create_list(:comment, 1, :against, commentable:) }

    it "shows the list of comments for the resource" do
      visit resource_path

      expect(page).to have_css("#comments")
      expect(page).to have_css(".comment", count: comments.length)

      within(".comments-two-columns") do
        check_comments_order(".comments-section__in-favor", comments_in_favor)
        check_comments_order(".comments-section__against", comments_against)
      end
    end
  end

  context "in mobile view" do
    it "shows kebab menu functionality" do
      resize_window_to_mobile
      visit resource_path

      within "#comment_#{comments.first.id}" do
        page.find("[id^='dropdown-trigger']").click
        expect(page).to have_css("ul.dropdown.dropdown__bottom", visible: :visible)
      end

      resize_window_to_desktop
    end

    context "when commentable is closed" do
      let!(:commentable) { closed_commentable }
      let!(:highest_voted_comment_in_favor) { create(:comment, :in_favor, commentable:, created_at: 2.days.ago, up_votes_count: 15) }
      let!(:high_voted_comment_in_favor) { create(:comment, :in_favor, commentable:, created_at: 4.days.ago, up_votes_count: 10) }
      let!(:older_comment_in_favor) { create(:comment, :in_favor, commentable:, created_at: 3.days.ago, up_votes_count: 5) }

      let!(:highest_voted_comment_against) { create(:comment, :against, commentable:, created_at: 2.days.ago, up_votes_count: 12) }
      let!(:high_voted_comment_against) { create(:comment, :against, commentable:, created_at: 5.days.ago, up_votes_count: 8) }
      let!(:older_comment_against) { create(:comment, :against, commentable:, created_at: 3.days.ago, up_votes_count: 4) }

      it "shows comments with top comments at the beginning and interleaved order after" do
        resize_window_to_mobile
        visit resource_path

        within(".comment-threads") do
          interleaved_comments = [
            highest_voted_comment_in_favor,
            highest_voted_comment_against,
            high_voted_comment_in_favor,
            high_voted_comment_against,
            older_comment_in_favor,
            older_comment_against
          ]

          all_comments = all(".comment-thread")

          interleaved_comments.each_with_index do |comment, index|
            expect(all_comments[index]).to have_content(comment.body["en"])
          end

          expect(page).to have_css(".most-upvoted-label", text: "Most upvoted", count: 2)
        end

        resize_window_to_desktop
      end
    end
  end

  context "when viewing a single comment" do
    let!(:single_comment) { create(:comment, commentable:, body: { "en" => "This is a single comment" }) }

    before do
      visit "#{resource_locator(commentable).path}?commentId=#{single_comment.id}"
    end

    it "displays only the single comment without columns" do
      expect(page).to have_css("#comments")
      expect(page).to have_css(".comment-thread", count: 1)
      expect(page).to have_content("This is a single comment")
      expect(page).to have_no_css(".comments-two-columns")
      expect(page).to have_no_content("In Favor")
      expect(page).to have_no_content("Against")
      expect(page).to have_no_content("You are viewing only one comment")
    end
  end

  context "when commentable is not closed" do
    let!(:oldest_in_favor_comment) { create(:comment, :in_favor, commentable:, created_at: 3.days.ago) }
    let!(:older_in_favor_comment) { create(:comment, :in_favor, commentable:, created_at: 2.days.ago) }
    let!(:oldest_against_comment) { create(:comment, :against, commentable:, created_at: 4.days.ago) }
    let!(:newer_against_comment) { create(:comment, :against, commentable:, created_at: 1.day.ago) }

    it "shows the comments in two columns sorted by creation date in ascending order" do
      visit resource_path

      within(".comments-two-columns") do
        check_comments_order(".comments-section__in-favor", [oldest_in_favor_comment, older_in_favor_comment])
        check_comments_order(".comments-section__against", [oldest_against_comment, newer_against_comment])
      end
    end

    it "allows the user to add a new comment at the end of the respective column" do
      visit resource_path

      add_new_comment("In favor", "This is a new comment in favor")

      within(".comments-section__in-favor") do
        expect(page).to have_content("This is a new comment in favor")
      end
    end

    it "disables the publish button until 'in favor' or 'against' is selected" do
      visit resource_path

      expect(page).to have_button("Publish comment", disabled: true)

      within(".comment__opinion-container") do
        click_on "In favor"
      end

      within "form#new_comment_for_#{commentable.commentable_type.demodulize}_#{commentable.id}" do
        fill_in_comment_field("This is a new comment in favor")
        click_on "Publish comment"
      end
    end

    it "shows comments sorted by creation date when viewed on a small screen" do
      resize_window_to_mobile
      visit resource_path

      within(".comment-threads") do
        comments = all(".comment-thread")

        expect(comments[0]).to have_content(oldest_in_favor_comment.body["en"])
        expect(comments[1]).to have_content(oldest_against_comment.body["en"])
        expect(comments[2]).to have_content(older_in_favor_comment.body["en"])
        expect(comments[3]).to have_content(newer_against_comment.body["en"])
      end

      resize_window_to_desktop
    end
  end

  context "when commentable is closed" do
    let!(:commentable) { closed_commentable }
    let!(:highest_voted_comment_in_favor) { create(:comment, :in_favor, commentable:, created_at: 2.days.ago, up_votes_count: 15) }
    let!(:high_voted_comment_in_favor) { create(:comment, :in_favor, commentable:, created_at: 4.days.ago, up_votes_count: 10) }
    let!(:older_comment_in_favor) { create(:comment, :in_favor, commentable:, created_at: 3.days.ago, up_votes_count: 5) }
    let!(:recent_comment_in_favor) { create(:comment, :in_favor, commentable:, created_at: 2.days.ago, up_votes_count: 3) }
    let!(:latest_comment_in_favor) { create(:comment, :in_favor, commentable:, created_at: 1.day.ago, up_votes_count: 1) }

    let!(:highest_voted_comment_against) { create(:comment, :against, commentable:, created_at: 2.days.ago, up_votes_count: 12) }
    let!(:high_voted_comment_against) { create(:comment, :against, commentable:, created_at: 5.days.ago, up_votes_count: 8) }
    let!(:older_comment_against) { create(:comment, :against, commentable:, created_at: 3.days.ago, up_votes_count: 4) }
    let!(:recent_comment_against) { create(:comment, :against, commentable:, created_at: 2.days.ago, up_votes_count: 2) }
    let!(:latest_comment_against) { create(:comment, :against, commentable:, created_at: 1.day.ago, up_votes_count: 1) }

    before do
      visit resource_path
    end

    it "shows the top voted comments at the top of each column, followed by comments in ascending chronological order" do
      within(".comments-two-columns") do
        check_comments_order(".comments-section__in-favor", [
                               highest_voted_comment_in_favor,
                               high_voted_comment_in_favor,
                               older_comment_in_favor,
                               recent_comment_in_favor,
                               latest_comment_in_favor
                             ])

        check_comments_order(".comments-section__against", [
                               highest_voted_comment_against,
                               high_voted_comment_against,
                               older_comment_against,
                               recent_comment_against,
                               latest_comment_against
                             ])
      end
    end

    it "shows the top voted comments with label at the top of each column" do
      within(".comments-two-columns") do
        within(".comments-section__in-favor") do
          expect(page).to have_css(".most-upvoted-label", text: "Most upvoted")
          expect(page).to have_content(highest_voted_comment_in_favor.body["en"])
        end

        within(".comments-section__against") do
          expect(page).to have_css(".most-upvoted-label", text: "Most upvoted")
          expect(page).to have_content(highest_voted_comment_against.body["en"])
        end
      end
    end
  end

  def check_comments_order(section_selector, comments)
    comments_section = all("#{section_selector} .comment-thread")
    comments.each_with_index do |comment, index|
      expect(comments_section[index]).to have_content(comment.body["en"])
    end
  end

  def add_new_comment(opinion, comment_text)
    within(".comment__opinion-container") do
      click_on opinion
    end

    within "form#new_comment_for_#{commentable.commentable_type.demodulize}_#{commentable.id}" do
      fill_in_comment_field(comment_text)
      click_on "Publish comment"
    end
  end

  def fill_in_comment_field(comment_text)
    field = find("#add-comment-#{commentable.commentable_type.demodulize}-#{commentable.id}")
    field.set " "
    field.native.send_keys comment_text
  end

  def resize_window_to_mobile
    page.driver.browser.manage.window.resize_to(375, 667)
  end

  def resize_window_to_desktop
    page.driver.browser.manage.window.resize_to(1920, 1080)
  end
end
